package net.helix.graphics;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.LinkedList;

import net.helix.core.transform.Transform;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector4f;

public class GraphicsHelper {
	static FloatBuffer projection = BufferUtils.createFloatBuffer(16);
	static FloatBuffer modelView = BufferUtils.createFloatBuffer(16);
	static IntBuffer viewport = BufferUtils.createIntBuffer(16);
	static FloatBuffer mouse3d = BufferUtils.createFloatBuffer(3);
	
	static int mint = 0;
	static ArrayList<Matrix4f> matrices = new ArrayList<Matrix4f>();
	
	public static Vector4f get3dMouse(float x, float y, float z){
		modelView.rewind();
		projection.rewind();
		viewport.rewind();
		
		GLU.gluUnProject(x, viewport.get(3) - y, z, modelView, projection, viewport, mouse3d);
		
		return new Vector4f(mouse3d.get(0), mouse3d.get(1), mouse3d.get(2), 0);
	}
	static FloatBuffer zReturnBuffer = BufferUtils.createFloatBuffer(1);
	public static Vector4f get3dMouse(float x, float y){
		modelView.rewind();
		projection.rewind();
		viewport.rewind();
		
		zReturnBuffer.rewind();
		
		FloatBuffer mouse3d = BufferUtils.createFloatBuffer(3);
		
		GL11.glReadPixels((int)x, (int)(viewport.get(3) - y), 1, 1, GL11.GL_DEPTH_COMPONENT,  GL11.GL_FLOAT, zReturnBuffer);
		
		GLU.gluUnProject(x, viewport.get(3) - y, zReturnBuffer.get(0), modelView, projection, viewport, mouse3d);
		
		return new Vector4f(mouse3d.get(0), mouse3d.get(1), mouse3d.get(2), 0);
	}
	
	public static float getMouseDepth(float x, float y){
		viewport.rewind();
		
		GL11.glReadPixels((int)x, (int)(viewport.get(3) - y), 1, 1, GL11.GL_DEPTH_COMPONENT,  GL11.GL_FLOAT, zReturnBuffer);
		
		return zReturnBuffer.get(0);
	}
	static FloatBuffer mouseOnScreenReturn = BufferUtils.createFloatBuffer(3);
	public static float getMouseDepthAt(float x, float y, float z){
		modelView.rewind();
		projection.rewind();
		viewport.rewind();
		
		mouseOnScreenReturn.rewind();
		
		GLU.gluProject(x, y, z, modelView, projection, viewport, mouseOnScreenReturn);
		
		return mouseOnScreenReturn.get(2); // returns only the depth value...
	}
	/*
	public static FloatBuffer texEvnFloatBuffer = BufferUtils.createFloatBuffer(4);
	static{
		texEvnFloatBuffer.put(0);
		texEvnFloatBuffer.put(0);
		texEvnFloatBuffer.put(0);
		texEvnFloatBuffer.put(0);
		texEvnFloatBuffer.flip();
	}
	*/
	static FloatBuffer frustrumPos = BufferUtils.createFloatBuffer(3);
	static Matrix4f calcMatrix = new Matrix4f();
	static Matrix4f calcMatrix2 = new Matrix4f();
	
	static float viewNear;
	static float viewFar;
	static float viewAspect;
	static float viewFOV;
	
	static Transform viewTransform;
	
	static float tang = (float) Math.atan(viewFOV*Math.PI/360);
	
	static float sphereFactorY = (float) (1f/Math.cos(viewFOV*Math.PI/360));
	static float sphereFactorX = (float) (1f/Math.cos(Math.atan(tang*viewAspect)));
	
	
	/*
	static Vector4f frustrumX = new Vector4f(1, 0, 0, 1);
	static Vector4f frustrumY = new Vector4f(0, 1, 0, 1);
	static Vector4f frustrumZ = new Vector4f(0 ,0, 1, 1);
	static Vector4f frustrumMid = new Vector4f(0 ,0, 0, 1);
	*/
	public static void setFrustrum(Transform v, float fov, float aspect, float near, float far){
		viewFOV = fov;
		viewAspect = aspect;
		viewNear = near;
		viewFar = far;
		
		viewTransform = v;
		
		modelView.rewind();
		projection.rewind();
		viewport.rewind();
		
		GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projection);
		GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, modelView);
		GL11.glGetInteger(GL11.GL_VIEWPORT, viewport);
		
		mint = 0;
		setMatrixAt(mint, viewTransform.matrix4f);
		
		tang = (float) Math.atan(viewFOV*Math.PI/360);
		
		sphereFactorY = (float) (1f/Math.cos(viewFOV*Math.PI/360));
		sphereFactorX = (float) (1f/Math.cos(Math.atan(tang*viewAspect)));
	}
	private static void setMatrixAt(int i, Matrix4f m){
		if(matrices.size() > i){
			Matrix4f mFrom = matrices.get(i);
			mFrom.load(m);
		}else{
			while(matrices.size() <= i){
				matrices.add(new Matrix4f());
			}
			setMatrixAt(i, m);
		}
	}
	private static Matrix4f getMatrixAt(int i){
		if(matrices.size() > i){
			return matrices.get(i);
		}else{
			while(matrices.size() <= i){
				matrices.add(new Matrix4f());
			}
			return matrices.get(i);
		}
	}
	public static Matrix4f getMatrixAtEnd(){
		return matrices.get(mint);
	}
	
	static Vector4f frustrumCheckVector = new Vector4f();
	public static boolean isPointInFrustrum(float x, float y, float z){
		// TODO: changes broke this, fix it later
		
		return isSphereInFrustrum(x, y, z, 0);
		
		/*
		modelView.rewind();
		projection.rewind();
		viewport.rewind();
		frustrumPos.rewind();
		GLU.gluProject(x, y, z, modelView, projection, viewport, frustrumPos);
		
		viewport.rewind();
		float w = viewport.get(2);
		float h = viewport.get(3);
		
		float px = frustrumPos.get();
		float py = frustrumPos.get();
		float pz = frustrumPos.get();
		*/
		/*
		frustrumCheckVector.x = x;
		frustrumCheckVector.y = y;
		frustrumCheckVector.z = z;
		frustrumCheckVector.w = 1;
		
		Matrix4f m = getMatrixAtEnd();
		
		Matrix4f.transform(m, frustrumCheckVector, frustrumCheckVector);
		
		frustrumCheckVector.w = (1.0f / frustrumCheckVector.w) * 0.5f;
		
		if(frustrumCheckVector.w == 0) return false;
		// x, y, and z will be between 0 and 1;
		frustrumCheckVector.x = frustrumCheckVector.x * frustrumCheckVector.w + 0.5f;
		frustrumCheckVector.y = frustrumCheckVector.y * frustrumCheckVector.w + 0.5f;
		frustrumCheckVector.z = frustrumCheckVector.z * frustrumCheckVector.w + 0.5f;
		
		//viewport.rewind();
		if(frustrumCheckVector.x >= 0 && frustrumCheckVector.x <= 1 && frustrumCheckVector.y >= 0 && frustrumCheckVector.y <= 1 && frustrumCheckVector.z >= 0 && frustrumCheckVector.z <= 1){
			return true;
		}else{
			return false;
		}
		*/
		
	}
	static Vector4f sphereCheckPos = new Vector4f();
	public static boolean isSphereInFrustrum(float x, float y, float z, float r){
		//sphereCheckPos.x = x - viewTransform.getX();
		//sphereCheckPos.y = y - viewTransform.getY();
		//sphereCheckPos.z = z - viewTransform.getZ();
		/*
		modelView.rewind();
		projection.rewind();
		viewport.rewind();
		frustrumPos.rewind();
		GLU.gluProject(x, y, z, modelView, projection, viewport, frustrumPos);
		
		viewport.rewind();
		float w = viewport.get(2);
		float h = viewport.get(3);
		
		float px = frustrumPos.get();
		float py = frustrumPos.get();
		float pz = frustrumPos.get();
		*/
		
		
		frustrumCheckVector.x = x;
		frustrumCheckVector.y = y;
		frustrumCheckVector.z = z;
		frustrumCheckVector.w = 1;
		
		Matrix4f m = getMatrixAtEnd();
		
		Matrix4f.transform(m, frustrumCheckVector, frustrumCheckVector);
		
		/*
		frustrumCheckVector.x = Vector4f.dot(frustrumX, sphereCheckPos);
		frustrumCheckVector.y = Vector4f.dot(frustrumY, sphereCheckPos);
		frustrumCheckVector.z = Vector4f.dot(frustrumZ, sphereCheckPos);
		*/
		
		frustrumCheckVector.z = -frustrumCheckVector.z;
		
		if ((frustrumCheckVector.z  > (viewFar + r)) || (frustrumCheckVector.z  < (viewNear - r))){
			return false;
		}
		
		float zz1 = frustrumCheckVector.z * tang * viewAspect;
		float d1 = sphereFactorX * r;
		if (frustrumCheckVector.x > zz1+d1 || frustrumCheckVector.x < -(zz1+d1)){
			return false;
		}
		
		float zz2 = frustrumCheckVector.z * tang;
		float d2 = sphereFactorY * r;
		if (frustrumCheckVector.y > zz2+d2 || frustrumCheckVector.y < -(zz2+d2)){
			return false;
		}
		
		/*
		// intersections
		// TODO: when NOT intersecting, then all the elements inside an object can be rendered.
		if (frustrumCheckVector.z > viewFar - r || frustrumCheckVector.z < viewNear+r)
			return true;
		if (frustrumCheckVector.y > zz2-d2 || frustrumCheckVector.y < -zz2+d2)
			return true;
		if (frustrumCheckVector.x > zz1-d1 || frustrumCheckVector.x < -zz1+d1)
			return true;
		*/
		return true;
		
	}
	public static boolean isCubeInFrustrum(float[][] cube){
		// TODO: finish this
		/*
		viewport.rewind();
		float w = viewport.get(2);
		float h = viewport.get(3);
		
		for(int i=0;i<8;i++){
			modelView.rewind();
			projection.rewind();
			viewport.rewind();
			frustrumPos.rewind();
			GLU.gluProject(cube[i][0], cube[i][1], cube[i][2], modelView, projection, viewport, frustrumPos);
		
			float px = frustrumPos.get();
			float py = frustrumPos.get();
			float pz = frustrumPos.get();
			
			
			
		}
		*/
		return true;
		
		
	}
	// TODO: if the modelView stack in openGL is exceeded, then do something smart about it.
	// Note, MatrixMode must be in model view
	public static void applyMatrix(Matrix4f m, FloatBuffer buff){
		Matrix4f min = getMatrixAtEnd();
		mint++;
		Matrix4f min2 = getMatrixAt(mint);
		Matrix4f.mul(min, m, min2);
		
		GL11.glPushMatrix();
		
		GL11.glMultMatrix(buff);
		
	}
	public static void unApplyMatrix(){
		mint--;
		if(mint < 0){
			throw new Error("Popped empty matrix stack!");
		}
		
		GL11.glPopMatrix();
	}
	public static void drawDube(){
		
		/*
		GL11.glVertex3f( 1, 1, 1); // 1
		GL11.glVertex3f( 1, 1,-1); // 2
		GL11.glVertex3f( 1,-1, 1); // 3
		GL11.glVertex3f( 1,-1,-1); // 4
		GL11.glVertex3f(-1, 1, 1); // 5
		GL11.glVertex3f(-1, 1,-1); // 6
		GL11.glVertex3f(-1,-1, 1); // 7
		GL11.glVertex3f(-1,-1,-1); // 8
		 */
		
		
		GL11.glBegin(GL11.GL_LINE_STRIP);
		
		 // 1 3 4 2
		GL11.glVertex3f( 1, 1, 1); // 1
		GL11.glVertex3f( 1,-1, 1); // 3
		GL11.glVertex3f( 1,-1,-1); // 4
		GL11.glVertex3f( 1, 1,-1); // 2
		GL11.glVertex3f( 1, 1, 1); // 1
		
		GL11.glEnd();
		GL11.glBegin(GL11.GL_LINE_STRIP);
		
		 // 5 7 8 6
		GL11.glVertex3f(-1, 1, 1); // 5
		GL11.glVertex3f(-1,-1, 1); // 7
		GL11.glVertex3f(-1,-1,-1); // 8
		GL11.glVertex3f(-1, 1,-1); // 6
		GL11.glVertex3f(-1, 1, 1); // 5
		
		GL11.glEnd();
		GL11.glBegin(GL11.GL_LINE_STRIP);
		
		 // 1 5 6 2
		GL11.glVertex3f( 1, 1, 1); // 1
		GL11.glVertex3f(-1, 1, 1); // 5
		GL11.glVertex3f(-1, 1,-1); // 6
		GL11.glVertex3f( 1, 1,-1); // 2
		GL11.glVertex3f( 1, 1, 1); // 1
		
		GL11.glEnd();
		GL11.glBegin(GL11.GL_LINE_STRIP);
		
		 // 3 7 8 4
		GL11.glVertex3f( 1,-1, 1); // 3
		GL11.glVertex3f(-1,-1, 1); // 7
		GL11.glVertex3f(-1,-1,-1); // 8
		GL11.glVertex3f( 1,-1,-1); // 4
		GL11.glVertex3f( 1,-1, 1); // 3
		
		GL11.glEnd();
		GL11.glBegin(GL11.GL_LINE_STRIP);
		
		 // 1 5 7 3
		GL11.glVertex3f( 1, 1, 1); // 1
		GL11.glVertex3f(-1, 1, 1); // 5
		GL11.glVertex3f(-1,-1, 1); // 7
		GL11.glVertex3f( 1,-1, 1); // 3
		GL11.glVertex3f( 1, 1, 1); // 1
		
		GL11.glEnd();
		GL11.glBegin(GL11.GL_LINE_STRIP);
		
		 // 2 6 8 4
		GL11.glVertex3f( 1, 1,-1); // 2
		GL11.glVertex3f(-1, 1,-1); // 6
		GL11.glVertex3f(-1,-1,-1); // 8
		GL11.glVertex3f( 1,-1,-1); // 4
		GL11.glVertex3f( 1, 1,-1); // 2
		
		GL11.glEnd();
	}
	public static void draw3dGrid(){
		GL11.glColor3f(.5f, .5f, .5f);
		
		
		GL11.glLineWidth(3);
		GL11.glBegin(GL11.GL_LINES);
		
		
		for(int x=-5;x<=5;x++){
			if(x == 0){
				GL11.glVertex3f(0, 0f, -5f*256f);
				GL11.glVertex3f(0, 0f,  -256f);
				
				GL11.glVertex3f(0, 0f,  256f);
				GL11.glVertex3f(0, 0f,  5f*256f);
			}else{
				GL11.glVertex3f(x*256f, 0f, -5f*256f);
				GL11.glVertex3f(x*256f, 0f,  5f*256f);
			}
		}
		
		for(int y=-5;y<=5;y++){
			if(y == 0){
				GL11.glVertex3f(-5f*256f, 0f, 0f);
				GL11.glVertex3f( -256f, 0f, 0f);
				
				GL11.glVertex3f( 256f, 0f, 0f);
				GL11.glVertex3f(5f*256f, 0f, 0f);
				
			}else{
				GL11.glVertex3f(-5f*256f, 0f, y*256f);
				GL11.glVertex3f( 5f*256f, 0f, y*256f);
			}
		}
		
		GL11.glEnd();
		
		GL11.glColor3f(.2f, .2f, .8f);
		GL11.glBegin(GL11.GL_LINES);
		GL11.glVertex3f(0f, 0f, 0f);
		GL11.glVertex3f(0f, 0f, 256f);
		GL11.glEnd();
		
		GL11.glColor3f(.2f, .8f, .2f);
		GL11.glBegin(GL11.GL_LINES);
		GL11.glVertex3f(0f, 0f, 0f);
		GL11.glVertex3f(0f, 256f, 0f);
		GL11.glEnd();
		
		GL11.glColor3f(.8f, .2f, .2f);
		GL11.glBegin(GL11.GL_LINES);
		GL11.glVertex3f(0f, 0f, 0f);
		GL11.glVertex3f(256f, 0f, 0f);
		GL11.glEnd();
		
		GL11.glColor3f(.5f, .5f, .5f);
		GL11.glBegin(GL11.GL_LINES);
		
		GL11.glVertex3f( 0f, 0f, 0f);
		GL11.glVertex3f( 0f, 0f,-256f);
		
		//GL11.glVertex3f( 0f, 0f, 0f);
		//GL11.glVertex3f( 0f,-256f, 0f);
		
		GL11.glVertex3f( 0f, 0f, 0f);
		GL11.glVertex3f(-256f, 0f, 0f);
		
		GL11.glEnd();
		
		
	}
}
